linux内核调试(三):内核崩溃日志抓取pstore 您所在的位置:网站首页 kernel_write 内核崩溃 linux内核调试(三):内核崩溃日志抓取pstore

linux内核调试(三):内核崩溃日志抓取pstore

2023-12-02 04:08| 来源: 网络整理| 查看: 265

1 pstore简介

pstore最初是用于系统发生oops或panic时,自动保存内核log buffer中的日志。不过在当前内核版本中,其已经支持了更多的功能,如保存console日志、ftrace消息和用户空间日志。

同时,它还支持将这些消息保存在不同的存储设备中,如内存、块设备或mtd设备【Memory Technology Devices】。 为了提高灵活性和可扩展性,pstore将以上功能分别抽象为前端和后端,其中像dmesg、console等为pstore提供数据的模块称为前端,而内存设备、块设备等用于存储数据的模块称为后端,pstore core则分别为它们提供相关的注册接口。

通过模块化的设计,实现了前端和后端的解耦,因此若某些模块需要利用pstore保存信息,就可以方便地向pstore添加新的前端。而若需要将pstore数据保存到新的存储设备上,也可以通过向其添加后端设备的方式完成。

在这里插入图片描述 除此之外,pstore还设计了一套pstore文件系统,用于查询和操作上一次重启时已经保存的pstore数据。当该文件系统被挂载时,保存在backend中的数据将被读取到pstore fs中,并以文件的形式显示。

2 代码实现 2.1 pstore初始化流程

pstore初始化主要是为其指定压缩算法,以及初始化pstore文件系统,其流程如下:

在这里插入图片描述

若存储空间比较有限,可以将pstore的内容压缩后再保存到backend设备上,相应的压缩算法可以通过配置选项CONFIG_PSTORE_COMPRESS_DEFAULT或模块参数compress指定。当前支持的压缩算法如下:

static const struct pstore_zbackend zbackends[] = { #if IS_ENABLED(CONFIG_PSTORE_DEFLATE_COMPRESS) { .zbufsize = zbufsize_deflate, .name = "deflate", }, #endif #if IS_ENABLED(CONFIG_PSTORE_LZO_COMPRESS) { .zbufsize = zbufsize_lzo, .name = "lzo", }, #endif #if IS_ENABLED(CONFIG_PSTORE_LZ4_COMPRESS) { .zbufsize = zbufsize_lz4, .name = "lz4", }, #endif #if IS_ENABLED(CONFIG_PSTORE_LZ4HC_COMPRESS) { .zbufsize = zbufsize_lz4, .name = "lz4hc", }, #endif #if IS_ENABLED(CONFIG_PSTORE_842_COMPRESS) { .zbufsize = zbufsize_842, .name = "842", }, #endif #if IS_ENABLED(CONFIG_PSTORE_ZSTD_COMPRESS) { .zbufsize = zbufsize_zstd, .name = "zstd", }, #endif { } }

pstore_init_fs用于初始化pstore文件系统,其代码如下:

int __init pstore_init_fs(void) { … err = sysfs_create_mount_point(fs_kobj, "pstore"); (1) … err = register_filesystem(&pstore_fs_type); (2) … }

(1)在/sys/fs目录下为其创建挂载点pstore

(2)向内核注册pstore文件系统

在pstore文件系统挂载时会调用pstore_fs_type的mount回调,它会执行挂载相关的操作,其流程如下: 在这里插入图片描述 其中pstore文件系统需要实现自身的超级块填充函数pstore_fill_super,它主要为其设置inode和file相关的操作函数,并且将上一次重启时已保存在pstore backend设备中的pstore信息读取出来,并加入文件系统中。其主要实现如下:

void pstore_get_backend_records(struct pstore_info *psi, struct dentry *root, int quiet) { … if (psi->open && psi->open(psi)) (1) goto out; for (; stop_loop; stop_loop--) { … pstore_record_init(record, psi); record->size = psi->read(record); (2) … decompress_record(record); (3) rc = pstore_mkfile(root, record); (4) … } if (psi->close) psi->close(psi); … }

(1)打开pstore backend设备

(2)从backend设备中逐条读取pstore record信息

(3)若使用了压缩算法,则需要将数据解压缩

(4)根据recored的内容,为其在pstore文件系统中创建一个文件。此后,用户就可以通过文件查询相关消息了

2.2 pstore backend注册流程

pstore core通过pstore_info结构体来描述一个pstore后端,该结构体主要包含了后端设备相关的信息,以及相关的操作函数,其定义如下:

struct pstore_info { struct module *owner; const char *name; (1) struct semaphore buf_lock; char *buf; (2) size_t bufsize; struct mutex read_mutex; int flags; (3) int max_reason; (4) void *data; (5) int (*open)(struct pstore_info *psi); (6) int (*close)(struct pstore_info *psi); ssize_t (*read)(struct pstore_record *record); int (*write)(struct pstore_record *record); int (*write_user)(struct pstore_record *record, const char __user *buf); int (*erase)(struct pstore_record *record); }

(1)后端设备的名字

(2)buf用于保存实际的消息

(3)flag用于表示消息的类型,其定义如下:

#define PSTORE_FLAGS_DMESG BIT(0) #define PSTORE_FLAGS_CONSOLE BIT(1) #define PSTORE_FLAGS_FTRACE BIT(2) #define PSTORE_FLAGS_PMSG BIT(3) (4)该参数只有前端为DMESG时才有效,用于控制哪些消息允许被dump到pstore中。只有dump reason小于等于该值的消息才会被dump,其reason取值如下: enum kmsg_dump_reason { KMSG_DUMP_UNDEF, KMSG_DUMP_PANIC, KMSG_DUMP_OOPS, KMSG_DUMP_EMERG, KMSG_DUMP_SHUTDOWN, KMSG_DUMP_MAX }

(5)后端设备的私有数据,可由回调函数传回

(6)后端设备相关的回调函数,如open和close用于打开和关闭设备,read、write用于读写pstore recored数据。write_user也用于向后端写入recored,但是其buffer为用户态地址,erase用于删除一个指定的recored

后端设备首先需要构造一个pstore_info结构体,然后调用pstore_register函数将其注册到pstore core中。以ram后端为例,其注册流程如下:

在这里插入图片描述 其中后端的配置信息可通过dts配置,如ramoops使用的内存地址、长度、recored长度等,ramoops_parse_dt用于解析dts中的相关信息。由于pstore可以包含多个前端,因此后端设备需要根据前端类型进行分区,ramoops_init_przs和ramoops_init_prz就是用于执行相关的分区操作。

pstore_register用于执行实际的注册工作,它的主要工作为校验输入参数是否合法,以及根据配置参数,注册相应的前端,其主要代码如下(去除了参数校验等部分):

int pstore_register(struct pstore_info *psi) { … if (psi->flags & PSTORE_FLAGS_DMESG) allocate_buf_for_compression(); (1) pstore_get_records(0); (2) if (psi->flags & PSTORE_FLAGS_DMESG) { pstore_dumper.max_reason = psinfo->max_reason; pstore_register_kmsg(); (3) } if (psi->flags & PSTORE_FLAGS_CONSOLE) pstore_register_console(); if (psi->flags & PSTORE_FLAGS_FTRACE) pstore_register_ftrace(); if (psi->flags & PSTORE_FLAGS_PMSG) pstore_register_pmsg(); … }

(1)若为pstore设置了压缩功能,则为其分配压缩时使用的内存

(2)从backend中读取上次重启时已保存的pstore recored,并为其建立相应的文件

(3)根据flags的值,注册相应的pstore前端

2.3 pstore frontend注册流程

pstore前端主要工作包含两部分,何时触发pstore写操作,以及需要向pstore写入什么内容,下面以dmesg前端为例,简要介绍一下其实现。

printk实现了一个kmsg_dump函数,用于方便其它模块dump内核的log buffer,当内核发生oops、panic或重启等事件时,都会调用该函数dump log信息。其代码实现如下:

void kmsg_dump(enum kmsg_dump_reason reason) { … list_for_each_entry_rcu(dumper, &dump_list, list) { (1) enum kmsg_dump_reason max_reason = dumper->max_reason; if (max_reason == KMSG_DUMP_UNDEF) { max_reason = always_kmsg_dump ? KMSG_DUMP_MAX : KMSG_DUMP_OOPS; } if (reason > max_reason) (2) continue; dumper->dump(dumper, reason); (3) } rcu_read_unlock(); }

(1)遍历dump_list链表中的所有的dumper,并对它们分别执行如下log dump操作

(2)根据dumper的max_reason值,确定是否需要向其dump log

(3)若需要dump,则调用该dumper的dump回调,执行实际的dump操作

因此pstore前端只需将自身注册到dump_list链表中即可,该操作由以下函数实现:

int kmsg_dump_register(struct kmsg_dumper *dumper) { … if (!dumper->registered) { dumper->registered = 1; list_add_tail_rcu(&dumper->list, &dump_list); err = 0; } … }

其中dmesg前端的主要工作就是为其实现一个dump函数,该函数将从log buffer中读取lou信息,然后将其封装为recored之后写入对应的后端设备,其主要定义如下:

static struct kmsg_dumper pstore_dumper = { .dump = pstore_dump, } static void pstore_dump(struct kmsg_dumper *dumper, enum kmsg_dump_reason reason) { … while (total < kmsg_bytes) { … pstore_record_init(&record, psinfo); (1) … header_size = snprintf(dst, dst_size, "%s#%d Part%u\n", why, oopscount, part); (2) dst_size -= header_size; if (!kmsg_dump_get_buffer(&iter, true, dst + header_size, dst_size, &dump_size)) (3) break; if (big_oops_buf) { zipped_len = pstore_compress(dst, psinfo->buf, header_size + dump_size, psinfo->bufsize); (4) … } else { record.size = header_size + dump_size; } ret = psinfo->write(&record); (5) … } … }

(1)初始化一个record结构体

(2)先向其写入pstore头信息,如dmesg reason、oops发生次数等

(3)从log buffer中读取一行log信息

(4)若需要压缩信息,则执行压缩操作

(5)调用后端的写函数,将record写入相应的后端设备

3 pstore配置和使用

pstore使用主要包含以下四个步骤:

(1)通过配置内核参数,使能pstore功能。由于dmesg前端是默认使能的,因此若以ramoops为后端,且使能所有前端为例,则其相应的配置参数如下: CONFIG_PSTORE=y CONFIG_PSTORE_RAM=y CONFIG_PSTORE_PMSG=y CONFIG_PSTORE_FTRACE=y CONFIG_PSTORE_CONSOLE=y (2)配置devicetree,为后端设备设置相关参数。以ramoops为例,pstore使用的内存需要被添加到reserved内存中,以下为其中的一个示例: reserved-memory { #address-cells = ; #size-cells = ; ranges; ramoops@21f00000 { compatible = "ramoops"; reg = ; record-size = ; console-size = ; ftrace-size = ; }; }; (3)挂载pstore文件系统,由于其挂载点为/sys/fs/pstore,因此相应的挂载操作如下:

mount -t pstore pstore /sys/fs/pstore

(4)挂载完成后,若后端中已经保存了先前的pstore消息,则可在/sys/fs/pstore目录下查看到对应的文件。用户可对该文件执行读取,删除等操作

写的真好,感谢前辈的优秀文章。

原文链接:https://zhuanlan.zhihu.com/p/545560128



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有